Comparatif des différents types de parallèlisme sur un gros Vision Transformer CoAtNet.

Le but de ce notebook est d'optimiser un code d'apprentissage d'un modèle CoAtNet-7 sur Imagenet pour Jean Zay en implémentant :
Les cellules dans ce notebook ne sont pas prévues pour être modifiées, sauf rares exceptions indiquées dans les commentaires. Les TP se feront en modifiant le code dlojz.py.
Les directives de modification seront marquées par l'étiquette TODO : dans le notebook suivant.
Les solutions sont présentes dans le répertoire solutions/.
Notebook rédigé par l'équipe assistance IA de l'IDRIS, septembre 2022
Un module PyTorch doit avoir été chargé pour le bon fonctionnement de ce Notebook. Nécessairement, le module pytorch-gpu/py3/1.11.0 :
!module list
Currently Loaded Modulefiles: 1) cuda/11.2 5) openmpi/4.1.1-cuda 9) sparsehash/2.0.3 2) nccl/2.9.6-1-cuda 6) intel-mkl/2020.4 10) libjpeg-turbo/2.1.3 3) cudnn/8.1.1.33-cuda 7) magma/2.5.4-cuda 11) pytorch-gpu/py3/1.11.0 4) gcc/8.4.1(8.3.1) 8) sox/14.4.2 >
Les fonctions python de gestion de queue SLURM dévelopées par l'IDRIS et les fonctions dédiées à la formation DLO-JZ sont à importer.
Le module d'environnement pour les jobs et la taille des images sont fixés pour ce notebook.
TODO : choisir un pseudonyme (maximum 5 caractères) pour vous différencier dans la queue SLURM et dans les outils collaboratifs pendant la formation.
from idr_pytools import display_slurm_queue, gpu_jobs_submitter, search_log
from dlojz_tools import controle_technique, compare, GPU_underthehood, plot_accuracy, lrfind_plot, pipe_memory, turbo_profiler
MODULE = 'pytorch-gpu/py3/1.13.0'
image_size = 224
account = 'for@v100'
name = 'pseudo' ## TODO Pseudonyme à choisir
Cette partie permet d'afficher et de gérer la queue SLURM.
Pour afficher toute la queue utilisateur :
display_slurm_queue()
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
1790091 gpu_p13 pseudo cfor032 R 0:52 1 r7i2n2
Done!
Remarque: Cette fonction utilisée plusieurs fois dans ce notebook permet d'afficher la queue de manière dynamique, rafraichie toutes les 5 secondes. Cependant elle ne s'arrête que lorsque la queue est vide. Si vous désirez reprendre la main sur le notebook, il vous suffira d'arrêter manuellement la cellule avec le bouton stop. Cela a bien sûr aucun impact sur le scheduler SLURM. Les jobs ne seront pas arrêtés.
Si vous voulez arrêter des jobs dans la queue :
#!scancel -u $USER
Cette partie debug permet d'afficher les fichiers de sortie et les fichiers d'erreur du job.
Il est nécessaire dans la cellule suivante (en décommentant) d'indiquer le jobid correspondant sous le format suivant.
*Remarque* : dans ce notebook, lorsque vous soumettrez un job, vous recevrez en retour le numéro du job dans le format suivant : jobid = ['123456']. La cellule ci-dessous peut ainsi être facilement actualisée.
jobid = ['2088207']
Fichier de sortie :
%cat {search_log(contains=jobid[0])[0]}
/bin/bash: -c: line 0: syntax error near unexpected token `('
/bin/bash: -c: line 0: `cat {search_log(contains=jobid[0])[0]}'
Fichier d'erreur :
%cat {search_log(contains=jobid[0], with_err=True)['stderr'][0]}
/bin/bash: -c: line 0: syntax error near unexpected token `('
/bin/bash: -c: line 0: `cat {search_log(contains=jobid[0], with_err=True)['stderr'][0]}'
Pour le debug ou pour comparer son code avec les solutions mises à disposition, la fonction suivante permet d'afficher une page html contenant un différentiel de fichiers texte.
s1 = "dlojz.py"
s2 = "./solutions/dlojz3_2.py"
compare(s1, s2)
Voir le résultat du différentiel de fichiers sur la page suivante (attention au spoil !) :
TODO : copier-coller la solution solutions/dlojz3_0.py dans dlojz.py afin d'ajouter dans le code les 2 éléments suivants nécessaires pour la suite des TP :
À noter : Pendant tout le TP, nous utiliserons une taille d'image de 352 x 352, qui correspond à la taille classique utilisée pour ce modèle.
Pour visualiser ces changements, veuillez utiliser le différentiel de fichiers suivant.
s1 = "dlojz.py"
s2 = "./solutions/dlojz3_0.py"
compare(s1, s2)
# copier/coller la solution si nécessaire
!cp solutions/dlojz3_0.py dlojz.py
Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
n_gpu = 1
batch_size = 32
command = f'CoAtNet/coatnet.py'
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task Submitted batch job 1790096 jobid = ['1790096']
Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.
Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.
jobid = ['1790096']
display_slurm_queue(name)
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
1790096 gpu_p13 pseudo cfor032 R 1:08 1 r7i0n2
Done!
%cat {search_log(name, contains=jobid[0])[0]}
CoAtNet 0: output shape = torch.Size([1, 1000]), N of Parameters = 20918064 CoAtNet 1: output shape = torch.Size([1, 1000]), N of Parameters = 39890496 CoAtNet 2: output shape = torch.Size([1, 1000]), N of Parameters = 64652672 CoAtNet 3: output shape = torch.Size([1, 1000]), N of Parameters = 131079872 CoAtNet 4: output shape = torch.Size([1, 1000]), N of Parameters = 228407456 CoAtNet 5: output shape = torch.Size([1, 1000]), N of Parameters = 610269344 CoAtNet 6: output shape = torch.Size([1, 1000]), N of Parameters = 1453918848 CoAtNet 7: output shape = torch.Size([1, 1000]), N of Parameters = 2362119808 Tue Feb 28 21:19:01 CET 2023
TODO : dans le script dlojz.py :
from CoAtNet.coatnet import coatnet_6
Remplacer :
model = models.resnet50() par model = coatnet_6((args.image_size,args.image_size))
et
archi_model = 'Resnet-50' par archi_model = 'CoAtNet-6'
Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
n_gpu = 4
batch_size = 4
command = f'dlojz.py -b {batch_size} --image-size {image_size} --test'
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 4 GPUs distributed on 1 nodes with 4 tasks / 4 gpus per node and 10 cpus per task Submitted batch job 1790206 jobid = ['1790206']
Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.
Puis, rebasculler la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.
jobid = ['1790206']
display_slurm_queue(name)
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
1790436 gpu_p13 pseudo cfor032 R 4:57 1 r8i5n0
1790437 gpu_p13 pseudo cfor032 R 4:57 1 r9i0n3
1790435 gpu_p13 pseudo cfor032 R 5:14 1 r9i6n5
Key interrupt
controle_technique(jobid)
Train throughput: 36.05 images/second GPU throughput: 36.07 images/second epoch time: 35538.12 seconds training time estimation for 90 epochs (with validations): 892.72 hours ----------- training step time average (fwd/bkwd on GPU): 0.443580 sec (23.9%/75.5%) +/- 0.029892 loading step time average (CPU to GPU): 0.000242 sec +/- 0.000025 ----------- ELIGIBLE to run 0 epochs
Afin de mesurer l'impact de la taille de batch sur l'occupation mémoire et sur le throughput, la cellule suivante permet de soumettre plusieurs jobs avec des tailles de batch croissantes. Dans les cas où la mémoire est saturée et dépasse la capacité du GPU, le système renverra une erreur CUDA Out of Memory.
Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
n_gpu = 1
batch_size = [2, 4, 6, 8]
command = [f'dlojz.py -b {b} --image-size {image_size} --test'
for b in batch_size]
jobids = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobids = {jobids}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task Submitted batch job 1790142 Submitted batch job 1790143 Submitted batch job 1790144 Submitted batch job 1790146 jobids = ['1790142', '1790143', '1790144', '1790146']
Copier-coller la sortie jobids = ['xxxxx', ...] dans la cellule suivante.
Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'eviter de relancer un job par erreur.
jobids = ['1790142', '1790143', '1790144', '1790146']
display_slurm_queue(name)
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
1790436 gpu_p13 pseudo cfor032 R 5:20 1 r8i5n0
1790437 gpu_p13 pseudo cfor032 R 5:20 1 r9i0n3
1790435 gpu_p13 pseudo cfor032 R 5:37 1 r9i6n5
Key interrupt
GPU_underthehood(jobids)
Batch size per GPU: 2 Max GPU Memory Allocated: 27.87 GB, Troughput: 6.275 images/second Batch size per GPU: 4 Max GPU Memory Allocated: 28.08 GB, Troughput: 10.831 images/second Batch size per GPU: 6 Max GPU Memory Allocated: 29.82 GB, Troughput: 14.203 images/second Batch size per GPU: 8 CUDA out of memory Memory occupancy by Model part : 27.429 +/- 0.223 GB

Ce TP consiste à implémenter le Pipelined Parallelism de PyTorch et de comparer cette solution avec les autres solutions.
La principale contrainte induite est de structurer le modèle comme suit, avec des torch.nn.Sequential pour chaque section et pour le modèle entier :

Le Pipeline Parallelism de PyTorch est de type standard GPipe.

À noter : Le code modifié permettra de faire de l'Hybrid Parallelism (DP + PP).
Chaque instance créée par Data Parallelism sera associée à une task Slurm, et chacune de ces instances pourra elle-même sollliciter plusieurs GPU pour tourner en mode Pipelined Parallelism.
Dans notre cas, nous testerons le code seulement en mode Pipelined Parallelism, sur 1 task associée à 4 GPU.
TODO : dans le script dlojz.py:
from torch.distributed.pipeline.sync import Pipe
import tempfile
from torch.distributed import rpc
--chunks (pour le nombre de micro batches) avant le parser les arguments.parser.add_argument('--chunks', default=1, type=int, help='number of chunks for Pipelined Parallelism')
args = parser.parse_args()
# Initialize RPC Framework, Pipe depends on it
tmpfile = tempfile.NamedTemporaryFile()
rpc.init_rpc(
name=f'worker{idr_torch.rank}',
rank=0,
world_size=1,
rpc_backend_options=rpc.TensorPipeRpcBackendOptions(
init_method="file://{}".format(tmpfile.name),
# Specifying _transports and _channels is a workaround and we no longer
# will have to specify _transports and _channels for PyTorch
# versions >= 1.8.1 (Not True for Jean Zay)
# With Jean Zay, _transports must be equal to ["shm", "uv"] and not ["ibv", "uv"]
_transports=["shm", "uv"],
_channels=["cuda_ipc", "cuda_basic"],
)
)
# define model
model = coatnet_6((args.image_size,args.image_size))
# How many sections
nb_part = torch.cuda.device_count()//int(os.environ['SLURM_NTASKS_PER_NODE'])
# device number where the first part of the model will run
first_part = idr_torch.local_rank*nb_part
# list of devices involved for pipelined Parallelism
gpus = [g for g in range(first_part, first_part+nb_part)]
class LambdaModule(torch.nn.Module):
def __init__(self, lambd):
super().__init__()
assert isinstance(lambd, type(lambda x: x))
self.lambd = lambd
def forward(self, x):
return self.lambd(x)
lambda_fc = LambdaModule(lambda x: x.view(-1, 2048))
section0 = torch.nn.Sequential(*model.s0, *model.s1, *model.s2, *model.pres3).to(gpus[0])
section1 = torch.nn.Sequential(*model.s3[:15]).to(gpus[1])
section2 = torch.nn.Sequential(*model.s3[15:30]).to(gpus[2])
section3 = torch.nn.Sequential(*model.s3[30:], *model.s4, model.pool, lambda_fc, model.fc).to(gpus[3])
pipe_model = torch.nn.Sequential(*section0, *section1, *section2, *section3)
# Pipe the model, chunks=n means that the batch (size according to batch size) will be shared to n micro batches (size = batch_size/chunks)
model = Pipe(pipe_model, chunks=args.chunks, checkpoint="never")
archi_model = 'CoAtNet-6'
DistributedDataParallel pour prendre en compte le fait qu'il y a plusieurs GPU associés à une seule task pour le Pipelined Parallelism, en indiquant simplement :model = DistributedDataParallel(model)
## Initialisation
if idr_torch.rank == 0: accuracies = []
val_loss = torch.Tensor([0.]).to(gpus[-1]) # send to GPU
val_accuracy = torch.Tensor([0.]).to(gpus[-1]) # send to GPU
# distribution of images and labels to all GPUs
images = images.to(gpus[0], non_blocking=args.non_blocking)
labels = labels.to(gpus[-1], non_blocking=args.non_blocking)
et
# distribution of images and labels to all GPUs
val_images = val_images.to(gpus[0], non_blocking=args.non_blocking)
val_labels = val_labels.to(gpus[-1], non_blocking=args.non_blocking)
Rref, il faudra utiliser la méthode .local_value() pour le transformer en tenseur pour le calcul de la loss, dans les boucles de training et de validation.# Runs the forward pass with autocasting.
with autocast():
outputs = model(images).local_value()
loss = criterion(outputs, labels)
et
# Runs the forward pass with no grade mode.
with torch.no_grad():
with autocast():
val_outputs = model(val_images).local_value()
loss = criterion(val_outputs, val_labels)
else: #
print(f'MaxMemory for GPU:{idr_torch.rank} {torch.cuda.max_memory_allocated()} Bytes')
#***********************************************************
for g in gpus: print(f'MaxMemory for GPU:{g} {torch.cuda.max_memory_allocated(device=g)} Bytes')
Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
n_gpu = 4
batch_size = 32
command = f'dlojz.py -b {batch_size} --image-size {image_size} --chunks 4 --test'
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name, n_gpu_per_task=4,
account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 4 GPUs distributed on 1 nodes with 1 tasks / 4 gpus per node and 40 cpus per task Submitted batch job 1790229 jobid = ['1790229']
Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.
Puis, rebasculler la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.
jobid = ['1790229']
display_slurm_queue(name)
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
1790436 gpu_p13 pseudo cfor032 R 5:39 1 r8i5n0
1790437 gpu_p13 pseudo cfor032 R 5:39 1 r9i0n3
1790435 gpu_p13 pseudo cfor032 R 5:56 1 r9i6n5
Key interrupt
controle_technique(jobid)
Train throughput: 13.68 images/second GPU throughput: 13.68 images/second epoch time: 93638.76 seconds training time estimation for 90 epochs (with validations): 2371.48 hours ----------- training step time average (fwd/bkwd on GPU): 2.338524 sec (33.9%/68.8%) +/- 0.078552 loading step time average (CPU to GPU): 0.000281 sec +/- 0.000037 ----------- ELIGIBLE to run 1 epochs
pipe_memory(jobid)
Afin de mesurer l'impact de la taille de batch sur l'occupation mémoire et sur le throughput, la cellule suivante permet de soumettre plusieurs jobs avec des tailles de batch croissantes. Dans les cas où la mémoire est saturée et dépasse la capacité du GPU, le système renverra une erreur CUDA Out of Memory.
Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
n_gpu = 4
chunks = [(16,2), (32,4), (48,6), (56,7), (64,8)]
command = [f'dlojz.py -b {c[0]} --image-size {image_size} --chunks {c[1]} --test'
for c in chunks]
jobids = gpu_jobs_submitter(command, n_gpu, MODULE, name=name, n_gpu_per_task=4,
account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobids = {jobids}')
batch job 0: 4 GPUs distributed on 1 nodes with 1 tasks / 4 gpus per node and 40 cpus per task Submitted batch job 1790435 Submitted batch job 1790436 Submitted batch job 1790437 Submitted batch job 1790438 jobids = ['1790435', '1790436', '1790437', '1790438']
Copier-coller la sortie jobids = ['xxxxx', ...] dans la cellule suivante.
Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'eviter de relancer un job par erreur.
jobids = ['1790423', '1790425', '1790428', '1790429', '1790430']
display_slurm_queue(name)
Done!
GPU_underthehood(jobids, calcul_memo=True)
Batch size per GPU: 16 Max GPU Memory Allocated: 8.38 GB, Troughput: 10.055 images/second Batch size per GPU: 32 Max GPU Memory Allocated: 15.74 GB, Troughput: 13.703 images/second Batch size per GPU: 48 Max GPU Memory Allocated: 23.21 GB, Troughput: 14.491 images/second Batch size per GPU: 56 Max GPU Memory Allocated: 26.94 GB, Troughput: 15.230 images/second Batch size per GPU: 64 CUDA out of memory Memory occupancy by Model part : 6.511 +/- 5.497 GB
controle_technique([jobids[-2]])
Train throughput: 15.23 images/second GPU throughput: 15.23 images/second epoch time: 84129.48 seconds training time estimation for 90 epochs (with validations): 2129.68 hours ----------- training step time average (fwd/bkwd on GPU): 3.677031 sec (32.4%/65.4%) +/- 0.116309 loading step time average (CPU to GPU): 0.000278 sec +/- 0.000037 ----------- ELIGIBLE to run 1 epochs

Ce TP consiste à implémenter Deepspeed pour intégrer l'optimisation ZeRO pour le Data Parallelism.
TODO : dans le script dlojz.py :
import deepspeed
# Include DeepSpeed configuration arguments
parser = deepspeed.add_config_arguments(parser)
À la place de :
# configure distribution method: define rank and initialise communication backend (NCCL)
dist.init_process_group(backend='nccl', init_method='env://',
world_size=idr_torch.size, rank=idr_torch.rank)
...
model = model.to(gpu)
...
model = DistributedDataParallel(model, device_ids=[idr_torch.local_rank])
...
mettre :
# Deepspeed initialization - force port number if several job run on the same node
deepspeed.init_distributed(distributed_port=os.environ['MASTER_PORT'])
model_engine, optimizer, _, scheduler = deepspeed.initialize(args=args,
model=model,
model_parameters=model.parameters()
)
À noter : Nous garderons, comme indiqué dans la documentation de Deepspeed, la distinction entre le modèle PyTorch model et le modèle encapsulé avec Deepspeed model_engine.
outputs = model_engine(images)
et
val_outputs = model_engine(val_images)
En effet, l'optimisation ZeRO ne supporte pas l'Automatic Mixed Precision. À la place, on appliquera une précision float16 à l'ensemble des paramètres du modèle (cela se fera dans la configuration json).
Pour retrouver les lignes de code à modifier dans le script dlojz.py, vous pouvez utiliser l'outil de différentiel de texte entre la solution dlojz1_1.py et la solution dlojz1_2.py.
float16 afin qu'elles correspondent à la précision du modèle :images = images.half().to(gpu, non_blocking=args.non_blocking)
et
val_images = val_images.half().to(gpu, non_blocking=args.non_blocking)
# backward and optimize
loss.backward()
optimizer.step()
par
#runs backpropagation
model_engine.backward(loss)
#weight update
model_engine.step()
# scheduler update
#scheduler.step()
La configuration de Deepspeed se fait par fichier JSON :
%%writefile ds_config.json
{ "train_micro_batch_size_per_gpu": 16,
"gradient _accumulation_steps": 1,
"optimizer": {
"type": "Adam",
"params": {
"lr": 0.001,
"weight_decay": 5e-4
}
},
"scheduler": {
"type": "OneCycle",
"params": {
"cycle_min_lr": 1e-6,
"cycle_max_lr": 1e-3,
"decay_lr_rate": 1e-6
}
},
"fp16": {
"enabled": true,
"loss_scale": 0,
"initial_scale_power": 32,
"loss_scale_window": 1000,
"hysteresis": 2,
"min_loss_scale": 1
},
"zero_optimization": {
"stage": 2
},
"zero_allow_untested_optimizer": true
}
Writing ds_config.json
Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
n_gpu = 4
batch_size = 16
command = f'dlojz.py -b {batch_size} --image-size {image_size} --test --deepspeed --deepspeed_config ds_config.json'
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
account=account, constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 4 GPUs distributed on 1 nodes with 4 tasks / 4 gpus per node and 10 cpus per task Submitted batch job 1790826 jobid = ['1790826']
Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.
Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'eviter de relancer un job par erreur.
jobid = ['1790826']
display_slurm_queue(name)
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
1790826 gpu_p13 pseudo cfor032 R 4:58 1 r8i6n8
Done!
controle_technique(jobid)
Train throughput: 87.96 images/second GPU throughput: 88.34 images/second epoch time: 14565.99 seconds training time estimation for 90 epochs (with validations): 368.32 hours ----------- training step time average (fwd/bkwd on GPU): 0.724443 sec (27.3%/65.7%) +/- 0.059703 loading step time average (CPU to GPU): 0.003166 sec +/- 0.002267 ----------- ELIGIBLE to run 1 epochs
pipe_memory(jobid)
Afin de mesurer l'impact de la taille de batch sur l'occupation mémoire et sur le throughput, la cellule suivante permet de soumettre plusieurs jobs avec des tailles de batch croissantes. Dans les cas où la mémoire est saturée et dépasse la capacité du GPU, le système renverra une erreur CUDA Out of Memory.
import json
batch_size = [2, 4, 8, 16, 24, 32]
for b in batch_size:
with open("ds_config.json", "r") as jsonFile:
data = json.load(jsonFile)
data["train_micro_batch_size_per_gpu"] = b
with open(f"ds_config{b}.json", "w") as jsonFile:
json.dump(data, jsonFile)
Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
n_gpu = 4
command = [f'dlojz.py -b {b} --image-size {image_size} --test --deepspeed --deepspeed_config ds_config{b}.json'
for b in batch_size]
jobids = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobids = {jobids}')
batch job 0: 4 GPUs distributed on 1 nodes with 4 tasks / 4 gpus per node and 10 cpus per task Submitted batch job 1790843 Submitted batch job 1790844 Submitted batch job 1790845 Submitted batch job 1790846 Submitted batch job 1790848 Submitted batch job 1790849 jobids = ['1790843', '1790844', '1790845', '1790846', '1790848', '1790849']
Copier-coller la sortie jobids = ['xxxxx', ...] dans la cellule suivante.
Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'eviter de relancer un job par erreur.
#jobids = ['169112', '169113', '169114', '169115', '169116', '169117']
display_slurm_queue(name)
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
1790848 gpu_p13 pseudo cfor032 R 3:17 1 r7i2n0
Done!
GPU_underthehood(jobids)
Batch size per GPU: 8 Max GPU Memory Allocated: 12.89 GB, Troughput: 26.103 images/second Batch size per GPU: 16 Max GPU Memory Allocated: 12.89 GB, Troughput: 45.338 images/second Batch size per GPU: 32 Max GPU Memory Allocated: 13.53 GB, Troughput: 68.211 images/second Batch size per GPU: 64 Max GPU Memory Allocated: 19.12 GB, Troughput: 90.026 images/second Batch size per GPU: 96 Max GPU Memory Allocated: 24.85 GB, Troughput: 97.052 images/second Batch size per GPU: 128 CUDA out of memory Memory occupancy by Model part : 11.023 +/- 2.194 GB
controle_technique([jobids[-2]])
Train throughput: 96.66 images/second GPU throughput: 97.05 images/second epoch time: 13254.92 seconds training time estimation for 90 epochs (with validations): 335.59 hours ----------- training step time average (fwd/bkwd on GPU): 0.989165 sec (29.8%/64.6%) +/- 0.069885 loading step time average (CPU to GPU): 0.004011 sec +/- 0.002290 ----------- ELIGIBLE to run 1 epochs

Ce TP consite à implémenter le Pipeline Parallelism de Deepspeed que l'on pourra ensuite utiliser en mode hybride avec le Data Parallelism + ZeRO.
La version du Pipelined Parallelism de Deepspeed est optimisé pour économiser l'empreinte mémoire.

À noter : Avec Deepspeed, le Pipelined Parallelism comme le Data Parallism fonctionne toujours en multi-task, ainsi une task est associée à chaque device.
L'implémentation du Pipeline Parallelism amenant trop de changements par rapport au code manipulé durant le TP, nous vous suggérons de copier-coller la solution solutions/dlojz3_4.py sur dlojz.py.
TODO :
solutions/dlojz3_4.py sur dlojz.py.# Define Pipeline Module
deepspeed.init_distributed(distributed_port=os.environ['MASTER_PORT'])
model = PipelineModule(layers = [
*model.s0, *model.s1, *model.s2, *model.pres3, *model.s3, *model.s4,
model.pool, lambda x: x.view(-1, 2048), model.fc],
num_stages = args.nb_pipeline_stages,
loss_fn=criterion,
partition_method = 'parameters' if args.partition_param else 'uniform')
# Deepspeed initialization - force port number if several job run on the same node
model_engine, optimizer, _, scheduler = deepspeed.initialize(args=args,
model=model,
model_parameters=model.parameters(),
training_data=train_dataset)
...
loss = model_engine.train_batch()
....
val_loss = model_engine.eval_batch(val_iter)
À noter : la configuration du Pipeline Parallelism se fait avec :
train_micro_batch_size_per_gpu correspondant à la taille du micro batch,gradient_accumulation_steps correspondant au nombre de tronçons du pipeline.La taille du mini batch pour chaque itération d'apprentissage correspond donc à train_micro_batch_size_per_gpu x gradient_accumulation_steps.
%%writefile ds_config.json
{ "train_micro_batch_size_per_gpu": 24,
"gradient_accumulation_steps": 8,
"optimizer": {
"type": "AdamW",
"params": {
"lr": 0.001,
"weight_decay": 5e-4
}
},
"scheduler": {
"type": "OneCycle",
"params": {
"cycle_min_lr": 1e-6,
"cycle_max_lr": 1e-3,
"decay_lr_rate": 1e-6
}
},
"fp16": {
"enabled": true,
"loss_scale": 0,
"initial_scale_power": 32,
"loss_scale_window": 1000,
"hysteresis": 2,
"min_loss_scale": 1
},
"zero_allow_untested_optimizer": true
}
Overwriting ds_config.json
Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
n_gpu = 4
command = f'dlojz.py --image-size {image_size} -p 4 --test --deepspeed --deepspeed_config ds_config.json'
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 4 GPUs distributed on 1 nodes with 4 tasks / 4 gpus per node and 10 cpus per task Submitted batch job 1791196 jobid = ['1791196']
Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.
Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'eviter de relancer un job par erreur.
#jobid = ['230538']
display_slurm_queue(name)
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
1791196 gpu_p13 pseudo cfor032 R 2:27 1 r7i2n2
Done!
controle_technique(jobid)
Train throughput: 76.31 images/second GPU throughput: 76.31 images/second epoch time: 16789.62 seconds training time estimation for 90 epochs (with validations): 426.16 hours ----------- training step time average (fwd/bkwd on GPU): 2.516051 sec (nan%/nan%) +/- 0.065642 loading step time average (CPU to GPU): 0.000002 sec +/- 0.000000 ----------- ELIGIBLE to run 1 epochs
pipe_memory(jobid)
Afin de mesurer l'impact de la taille de batch sur l'occupation mémoire et sur le throughput, la cellule suivante permet de soumettre plusieurs jobs avec des tailles de batch croissantes. Dans les cas où la mémoire est saturée et dépasse la capacité du GPU, le système renverra une erreur CUDA Out of Memory.
import json
chunks_numbers = [2, 4, 8, 16, 32, 40]
for c in chunks_numbers:
with open("ds_config.json", "r") as jsonFile:
data = json.load(jsonFile)
data["gradient_accumulation_steps"] = c
with open(f"ds_config{c}.json", "w") as jsonFile:
json.dump(data, jsonFile)
Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
n_gpu = 4
command = [f'dlojz.py --image-size {image_size} -p 4 --test --deepspeed --deepspeed_config ds_config{c}.json'
for c in chunks_numbers]
jobids = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
account=account, time_max='00:20:00', constraint='v100-32g')
print(f'jobids = {jobids}')
batch job 0: 4 GPUs distributed on 1 nodes with 4 tasks / 4 gpus per node and 10 cpus per task Submitted batch job 1791309 Submitted batch job 1791310 Submitted batch job 1791311 Submitted batch job 1791312 Submitted batch job 1791313 Submitted batch job 1791314 jobids = ['1791309', '1791310', '1791311', '1791312', '1791313', '1791314']
Copier-coller la sortie jobids = ['xxxxx', ...] dans la cellule suivante.
Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'eviter de relancer un job par erreur.
#jobids = ['239664', '239666', '239667', '239668', '239674', '239676']
display_slurm_queue(name)
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
1791314 gpu_p13 pseudo cfor032 R 7:31 1 r9i0n2
Done!
GPU_underthehood(jobids, calcul_memo=True)
Batch size per GPU: 48 Max GPU Memory Allocated: 11.67 GB, Troughput: 47.323 images/second Batch size per GPU: 96 Max GPU Memory Allocated: 22.68 GB, Troughput: 62.558 images/second Batch size per GPU: 192 Max GPU Memory Allocated: 22.79 GB, Troughput: 76.255 images/second Batch size per GPU: 384 Max GPU Memory Allocated: 22.79 GB, Troughput: 83.464 images/second Batch size per GPU: 768 Max GPU Memory Allocated: 22.79 GB, Troughput: 91.084 images/second Batch size per GPU: 960 Max GPU Memory Allocated: 22.79 GB, Troughput: 89.427 images/second Memory occupancy by Model part : 18.320 +/- 8.831 GB
controle_technique([jobids[-1]])
Train throughput: 89.43 images/second GPU throughput: 89.43 images/second epoch time: 14331.18 seconds training time estimation for 90 epochs (with validations): 363.55 hours ----------- training step time average (fwd/bkwd on GPU): 10.734963 sec (nan%/nan%) +/- 0.253616 loading step time average (CPU to GPU): 0.000002 sec +/- 0.000000 ----------- ELIGIBLE to run 1 epochs
TODO : Trouver la meilleure architecture et configuration en terme de Throughput.
L'argument -p correspond au nombre de stages du pipeline. Sachant que l'on utilise 4 GPU, un stage de 4 correspond à un pipeline parallelism total sur 4 GPU, un stage de 2 correspond à un hybrid parallelism 2x2, un stage de 1 à un Data Parallelism complet.
Choisir un optimiseur accéléré comme : Adam, AdamW, Lamb, OnebitAdam, OnebitLamb, ou ZeroOneAdam.
Configuration JSON :
À noter :
OnebitAdam, OnebitLamb, ou ZeroOneAdam ne marche pas avec ZeRO. Si vous utilisez un de ceux-ci, il faudra mettre le paramètre freeze_step comme ceci pour pouvoir mesurer son accélération dans notre test :"optimizer": {
"type": "OnebitAdam",
"params": {
"lr": 0.001,
"weight_decay": 5e-4,
"freeze_step": 5
}
},
%%writefile ds_config.json
{ "train_micro_batch_size_per_gpu": 16,
"gradient_accumulation_steps": 24,
"optimizer": {
"type": "AdamW",
"params": {
"lr": 0.001,
"weight_decay": 5e-2
}
},
"scheduler": {
"type": "OneCycle",
"params": {
"cycle_min_lr": 1e-6,
"cycle_max_lr": 1e-3,
"decay_lr_rate": 1e-6
}
},
"fp16": {
"enabled": true,
"loss_scale": 0,
"initial_scale_power": 32,
"loss_scale_window": 1000,
"hysteresis": 2,
"min_loss_scale": 1
},
"zero_optimization": {
"stage": 1
},
"zero_allow_untested_optimizer": true
}
Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
Copier-coller vos sorties jobid = ['xxxxx'] dans la cellule suivante.
#jobid = ['202876']
display_slurm_queue(name)
controle_technique(jobid)
pipe_memory(jobid)
%cat {search_log(contains=jobid[0])[0]}
%cat {search_log(contains=jobid[0], with_err=True)['stderr'][0]}
